package com.feedgraph;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.LoaderManager;


import android.content.Context;
import android.net.Uri;
import android.text.format.Time;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListAdapter;

/**
 * Allows an Activity to subscribe to FeedGraph content.
 * 
 *  Use {@link FeedManager.makeFeed} if you want low-level access to feed content.
 *  
 *  Use {@link FeedManager.makeLoader} if you want to display feed content in an ListView
 *  or an Activity which supports AsyncTaskLoader.
 * 
 * If HttpResponseCache is installed, FeedManager will automatically cache responses
 */
public class FeedManager {
	
	public interface FeedItemAdapter {
		View getView(Context context, View view, ViewGroup parent);
		int getViewType();

	}

	public static final String API_BASE_URL = "http://api.feedgraph.com/";

	protected Context mContext; 
	private long count = 0;
	
	/**
	 * Construct a FeedManager in the onCreate method of an Activity.
	 * @param context
	 */
	public FeedManager(Context context) {
		mContext = context;
	}
	
	public Context getContext () {
		return mContext;
	}
	
	public void postItem(Uri uri, JSONObject json) throws IOException {
		makeFeed(uri).postItem (json);
	}
	
	public void postItem(String path, JSONObject json) throws IOException {
		makeFeed(path).postItem(json);
	}
	
	/**
	 * Constructs a {@link Feed} object representing the content at the given URI.
	 * @param uri
	 * @return
	 */
	public Feed makeFeed(Uri uri) {
		return new Feed(this, uri);
	}
	
	/**
	 * Constructs a @{link Feed} object representing content at http://api.feedgraph.com/{path}
	 * @param path
	 * @return
	 */
	public Feed makeFeed(String path) {
		
		if (path.startsWith ("/")) path = path.substring (1);
		
		return makeFeed(Uri.parse (API_BASE_URL + path));
	}
	
	protected FeedItem parseFeedItem(JSONObject json) throws JSONException {
		return new FeedItem(json, count++);
	}
	
	/**
	 * Constructs an {@link AsyncFeedLoader} which can be used for loading content into
	 * any Activity implementing {@link LoaderCallbacks}
	 * 
	 * @param uri
	 * @return
	 */
	public AsyncFeedLoader makeLoader() {
		
		return new AsyncFeedLoader(mContext, this);
		
	}
	
	public AsyncFeedLoader makeLoader(Uri uri) {
		AsyncFeedLoader loader = new AsyncFeedLoader(mContext, this);
		loader.addFeed (uri, false);
		
		return loader;
	}
	
	/**
	 * Constructs an {@link AsyncFeedLoader} which can be used for loading content into
	 * any Activity implementing @{link LoaderCallbacks}
	 * 
	 * @param path
	 * @return
	 */
	public AsyncFeedLoader makeLoader(String path) {
		
		return makeLoader(Uri.parse (API_BASE_URL + path));
		
	}
	
	public static class FeedItem {
		
		
		protected UUID mUUID;
		protected long mLocalID;
		protected Time mCreatedAt;
		protected JSONObject mObject;
		protected FeedItemAdapter mAdapter;
		
		private FeedItem (JSONObject json, long localID) throws JSONException {
			mUUID = new UUID(json.getString ("ID"));
			mCreatedAt = new Time();
			mCreatedAt.parse3339(json.getString("CreatedAt"));
			mObject = json.getJSONObject ("Object");
			mLocalID = localID;
		}
		
		public UUID getUUID() {
			return mUUID;
		}
		
		public long getID() {
			return mLocalID;
		}
		
		public Time getCreatedAt() {
			return mCreatedAt;
		}
		
		public JSONObject getObject() {
			return mObject;
		}
		
		public FeedItemAdapter getAdapter() {
			return mAdapter;
		}
		
		public void setAdapter(FeedItemAdapter adapter) {
			mAdapter = adapter;
		}
	}
	
	public static class BasicFeed extends LinkedList <FeedItem> {
		

		/**
		 * 
		 */
		private static final long serialVersionUID = 5735693906511076338L;
		protected FeedManager mFeedManager;

		protected BasicFeed (FeedManager manager) {
			mFeedManager = manager;
		}
		
		protected int fetch(Uri uri, boolean ignoreOld) throws IOException, JSONException {
			
			Uri.Builder builder = uri.buildUpon ();
			
			if (ignoreOld) {
				Time time = new Time();
				time.setToNow ();
				builder.appendQueryParameter ("since", time.format3339 (false));	
			}
						
			Uri uri2 = builder.build ();
			
			Log.d(getClass().getSimpleName (), "fetching from " + uri2.toString ());
			
			URLConnection conn = new URL(uri2.toString ()).openConnection ();
			
			String str = IOUtil.toString (conn.getInputStream ());

			JSONArray json = new JSONArray (str);

			for (int i = 0; i < json.length (); i++) {
				FeedItem item = mFeedManager.parseFeedItem (json.getJSONObject (i));
				add (item);
			}
			
			return json.length();
		}
	}
	
	/**
	 * Represents a stream of items which can be pushed through {@link http://FeedGraph.com}.
	 * 
	 * Each feed is stored at a URI such as {@link http://api.feedgraph.com/test/foo/bar}
	 * @author ryanne
	 *
	 */
	public static class Feed extends BasicFeed {

		/**
		 * 
		 */
		private static final long serialVersionUID = 6938189920589601828L;

		// URI to this feed
		protected Uri mUri;
		private Time version = null;
		
		private Feed(FeedManager feedManager, Uri uri) {
			super(feedManager);
			mUri = uri;
			mFeedManager = feedManager;
		}

		/**
		 * Fetch the latest content of the feed, ignoring anything earlier than the given timestamp.
		 * @param since any content older than this timestamp is ignored
		 * @param waitForNewContent if true, this method will block (not return) until new content is available on the server
		 * @return
		 * @throws IOException
		 * @throws JSONException
		 */
		public int fetchSince(Time since, boolean waitForNewItems) throws IOException, JSONException {
			
			Uri.Builder builder = mUri.buildUpon ();
			
			if (since != null) {
				builder.appendQueryParameter ("since", since.format3339 (false));	
			}
			
			if (waitForNewItems) {
				builder.appendQueryParameter("block", "true");
			}
			
			Uri uri = builder.build ();
			
			Log.d(getClass().getSimpleName (), "fetching from " + uri.toString ());
			
			URLConnection conn = new URL(uri.toString ()).openConnection ();
			
			String str = IOUtil.toString (conn.getInputStream ());

			JSONArray json = new JSONArray (str);

			for (int i = 0; i < json.length (); i++) {
				FeedItem item = mFeedManager.parseFeedItem (json.getJSONObject (i));
				add (item);
				
				if (i == json.length() - 1) {
					version = item.mCreatedAt;
				}
			}
			
			return json.length();
		}
		
		/**
		 * (re-)fetches all content currently stored on the server for this feed.  Note that older content
		 * is eventually forgotten by FeedGraph.com, and that there may be no new content since the previous
		 * time this method was called.  This method may cause older {@FeedItem}s to disappear from this Feed object.
		 * @return number of new {@link FeedItem}s
		 * @throws IOException
		 * @throws JSONException
		 */
		public int fetchAll() throws IOException, JSONException {
			return fetchSince(null, false);
		}
		
		/**
		 * Fetch any new content, if it exists.
		 * @return number of new {@link FeedItem}s
		 * @throws IOException
		 * @throws JSONException
		 */
		public int fetchNew() throws IOException, JSONException {
			if (version == null) {
				return fetchAll();
			} 
			return fetchSince(version, false);
		}
		
		/**
		 * Blocks until new content is available on the server. Returns the number of new {@link FeedItem}s.
		 * @return number of new {@link FeedItem}s
		 * @throws IOException
		 * @throws JSONException
		 */
		public int waitForNewItems() throws IOException, JSONException {
			if (version == null) {
				return fetchAll();
			} 
			return fetchSince(version, true);
		}
		
		/**
		 * Publish a JSON object to this feed, causing a new FeedItem to be created on the server.
		 * @param json
		 * @throws MalformedURLException
		 * @throws IOException
		 */
		public void postItem(JSONObject json) throws IOException {
			
			Log.d(getClass().getSimpleName (), "posting to " + mUri.toString ());
			Log.d(getClass().getSimpleName (), json.toString ());
			
			byte [] bytes = json.toString().getBytes ();
			
			HttpURLConnection conn = (HttpURLConnection) new URL(mUri.toString ()).openConnection ();
			conn.setDoOutput(true);
      conn.setRequestProperty("Content-Type", "application/json");
      conn.setRequestMethod("POST");    

			OutputStream out = conn.getOutputStream ();
			out.write (bytes);
			out.flush ();
			out.close ();
 
			if (conn.getResponseCode () != 200) {
				throw new IOException(conn.getResponseMessage ());
			}
		}

	}

	public static class UUID {
		public String mID;
		
		private UUID(String id) {
			mID = id;
		}
		
		/* convert UUID to string */
		public String toString() {
			return mID;
		}
	}
	
	public interface LoaderCallbacks extends LoaderManager.LoaderCallbacks<BasicFeed> {
		
	}
}
